package mac10.models;

import java.awt.Color;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedList;

import mac10.bean.MasterBean;
import mac10.util.AStarHeapCow;
import mac10.util.Node;
import de.dailab.jiactng.agentcore.ontology.IAgentDescription;

/**
 * @author fl
 * 
 */

public class Cluster {
	private ArrayList<Cell> list;
	private Node start;
	private ArrayList<Point> drivePoints;
	private HashMap<IAgentDescription, Point> shepherdPoints;
	private String id;
	private Point center, oppositeDirection, densityCenter;
	public HashMap<IAgentDescription, Cell> shepherds = new HashMap<IAgentDescription, Cell>();
	public Color clusterColor;
	
	public int smoothValue = 0;
	
	public LinkedList<String> pathToCorral = null;
	
	public int getPathLengthToCorral() {
		if (pathToCorral != null) {
			return pathToCorral.size();
		}
		return Integer.MAX_VALUE;
	}

	public void setClusterColor(Color c) {
		this.clusterColor = c;
		for (Cell cell : list) {
			cell.setClusterColor(c);
		}
	}

	public Node getStart() {
		return start;
	}

	public String getId() {
		return id;
	}

	public void setId(String id) {
		this.id = id;
	}

	public Cluster(ArrayList<Cell> list) {

		this.list = list;

	}

	public ArrayList<Cell> getList() {
		return list;
	}

	public int size() {
		return list.size();
	}

	public ArrayList<Integer> getIdList() {
		ArrayList<Integer> ids = new ArrayList<Integer>();

		for (Cell c : list) {

			if (c == null || c.getCowId() == -1)
				System.err.println("Cluster cowId = -1");

			ids.add(c.getCowId());
		}

		return ids;
	}

	public int shepherdsNeeded() {
		if (list.size() == 1) return 1;
		if (list.size() > 1 && list.size() < 6) return 2;
		if (list.size() > 5 && list.size() < 16) return 3;
		return 4;
	}

	public synchronized void updatePostions() {
		ArrayList<Integer> ids = getIdList();
		ArrayList<Cell> newList = new ArrayList<Cell>();
		HashMap<Integer, Cell> cows = WorldModel.staticWorldModel.getCows();

		for (Integer integer : ids) {
			if (cows.get(integer) != null)
				newList.add(cows.get(integer));
		}
		list = newList;

		if (list.size() < 1)
			return;

		calcCenter();
		
		// by moekon
		shepherdPoints = null;
		drivePoints = null;
	}

	public void setList(ArrayList<Cell> list) {
		this.list = list;
	}

	public void add(Cell c) {
		list.add(c);
	}

	public Point getDensityCenter() {
		if (densityCenter != null)
			return densityCenter;
		else
			return calcCenter();
	}

	// private Point calcDensityCenter ()
	// {
	// int x = 0;
	// int y = 0;
	// for (Cell cell : list)
	// {
	// x += cell.getX();
	// y += cell.getY();
	// }
	// x /= list.size();
	// y /= list.size();
	//
	// densityCenter = new Point(x, y);
	// return densityCenter;
	// }

	public Point getCenter() {
		if (center != null)
			return center;
		else
			return calcCenter();
	}

	private Point calcCenter() {
		int minx = Integer.MAX_VALUE;
		int maxx = Integer.MIN_VALUE;
		int miny = Integer.MAX_VALUE;
		int maxy = Integer.MIN_VALUE;

		for (Cell cell : list) {
			if (minx > cell.x)
				minx = cell.x;
			if (maxx < cell.x)
				maxx = cell.x;
			if (miny > cell.y)
				miny = cell.y;
			if (maxy < cell.y)
				maxy = cell.y;
		}
		center = new Point((minx + maxx) / 2, (miny + maxy) / 2);
		return center;
	}

	public Point[] calcAnchorPoint(WorldModel worldModel, long deadline) {
		Point center = getCenter();
		Point opd = getOppositeDirection(worldModel, deadline);
		if (opd == null) {
			return null;
		}
		double normOpd = opd.euklNorm(Point.ZERO);
		double maxl = Double.NEGATIVE_INFINITY;
		Point lastCow = null;
		for (Point cow : list) {
			Point tmp = new Point(cow.x - center.x, cow.y - center.y);
			double l = opd.euklScalarProduct(tmp) / normOpd;
			if (maxl < l) {
				maxl = l;
				lastCow = new Point(cow);
			}
		}
		maxl = maxl / normOpd;
		int anchorx = center.x + (int) (opd.x * maxl + 0.5);
		int anchory = center.y + (int) (opd.y * maxl + 0.5);
		Point anchor = new Point(worldModel.getNearstCellInGrid(anchorx,
				anchory));
		worldModel.getCell(anchor).setMarked(Cell.RED_CROSS);
		worldModel.getCell(lastCow).setMarked(Cell.MAGENTA_CIRCLE);

		// System.out.println(id+" size "+list.size()+" center "+center+" lastcow "+lastCow+" anchor "+anchor+" maxl "+maxl+" opd "+opd+" normOpd "+normOpd);

		return new Point[] { anchor, lastCow };
	}

	public ArrayList<Point> calcDriveLineLastCow(int lineLength, int gap,
			Point anchor, Point lastCow, Point opd, WorldModel worldModel) {
		anchor = new Point(worldModel.getNearstCellInGrid(anchor.x + gap
				* opd.x, anchor.y + gap * opd.y));
		lastCow = new Point(worldModel.getNearstCellInGrid(lastCow.x + gap
				* opd.x, lastCow.y + gap * opd.y));
		ArrayList<Point> driveLine = new ArrayList<Point>(lineLength);
		// Line direction
		Point ld = new Point((int) Math.signum(anchor.x - lastCow.x),
				(int) Math.signum(anchor.y - lastCow.y));

		for (int i = 0; i < lineLength; i++) {
			Point tmp = new Point(lastCow.x + i * ld.x, lastCow.y + i * ld.y);
			if (worldModel.isInGrid(tmp)
					&& !worldModel.getCell(tmp).isObstacle()
					&& !worldModel.getCell(tmp).isSwitch())
				driveLine.add(tmp);
			else
				break;
		}

		return driveLine;
	}

	private ArrayList<Point> calcDriveLineAnchor(int lineLength, int gap,
			Point anchor, Point opd, WorldModel worldModel) {

		ArrayList<Point> driveLine = new ArrayList<Point>(lineLength);
		driveLine.add(anchor);
		for (int i = 0; i < lineLength / 2; i++) {
			Point tmp = new Point(anchor.x + i * opd.y, anchor.y - i * opd.x);
			if (worldModel.isInGrid(tmp)
					&& !worldModel.getCell(tmp).isObstacle()
					&& !worldModel.getCell(tmp).isSwitch())
				driveLine.add(tmp);
			else
				break;
		}
		// Umsortierung der driveLine damait am Ende die punkte von einem Ende
		// zum andern durchgehen
		// Zur Vereinfachung der drivePoint berechnung
		ArrayList<Point> foo = new ArrayList<Point>();
		for (int i = driveLine.size() - 1; i >= 0; i--) {
			foo.add(driveLine.get(i));
		}
		driveLine = foo;

		for (int i = 0; i < lineLength / 2; i++) {
			Point tmp = new Point(anchor.x - i * opd.y, anchor.y + i * opd.x);
			if (worldModel.isInGrid(tmp)
					&& !worldModel.getCell(tmp).isObstacle()
					&& !worldModel.getCell(tmp).isSwitch())
				driveLine.add(tmp);
			else
				break;
		}

		return driveLine;
	}

	private ArrayList<Point> calcDriveLineOneShepherd(int gap, Point lastCow,
			Point opd, WorldModel worldModel) {
		ArrayList<Point> driveLine = new ArrayList<Point>(1);
		driveLine.add(new Point(worldModel.getNearstCellInGrid(lastCow.x + gap
				* opd.x, lastCow.y + gap * opd.y)));
		return driveLine;
	}

	private ArrayList<Point> calcDriveLine(WorldModel worldModel, long deadline) {
		Point[] foobar = calcAnchorPoint(worldModel, deadline);
		if (foobar == null)
			return null;
		Point anchor = foobar[0];
		Point lastCow = foobar[1];
		Point opd = getOppositeDirection(worldModel, deadline);
		if (opd == null)
			return null;
		int gap = 1;
		int lineLength = shepherds.size() * 3 - 2;
		ArrayList<Point> driveLine;
		if (shepherds.size() == 1) {
			driveLine = calcDriveLineOneShepherd(gap, lastCow, opd, worldModel);
		} else if (anchor.maxNorm(lastCow) < 3) {
			driveLine = calcDriveLineAnchor(lineLength, gap, anchor, opd,
					worldModel);
			System.out.println(this.id + "Ancor Line size " + lineLength);
		} else {
			driveLine = calcDriveLineLastCow(lineLength, gap, anchor, lastCow,
					opd, worldModel);

			System.out.println(this.id + "Last Line size " + lineLength);
		}

		for (Point point : driveLine) {
			worldModel.getCell(point).setMarked(Cell.BLACK_CROSS);
		}
		MasterBean.newMonitor.repaint();
		return driveLine;
	}

	private ArrayList<Point> calcDrivePoints(WorldModel worldModel,
			long deadline) {
		ArrayList<Point> driveLine = calcDriveLine(worldModel, deadline);
		if (driveLine == null)
			return null;
		ArrayList<Point> drivePoints = new ArrayList<Point>();

		if (shepherds.size() == 1) {
			drivePoints.add(driveLine.get(0));
			return drivePoints;
		}
		// TODO sheinen h�ufig oder sogar immer zu wenige punkte zu sein
		int gap = (int) (driveLine.size() / shepherds.size());
		// System.out.println(id+" gap "+gap);
		
		for (int i = 0; i < shepherds.size() && i * gap < driveLine.size(); i++) {
		  drivePoints.add(driveLine.get((int) (i * gap)));
			// System.out.println(id +" point an "+((int) (i*gap)));
		}
		return drivePoints;
	}

	public Point getDrivePoint(WorldModel worldModel,
			IAgentDescription agent, long deadline) {
		// if(true)
		// return Point.ZERO;
	  
	  long start,end;
	  
		if (!shepherds.containsKey(agent)) {
			System.err.println("Custer: " + id
					+ " doesn't know about shepherd: " + agent.getName());
			return null;
		}

		if (drivePoints == null) {
			
      start = System.currentTimeMillis();		  
		  drivePoints = calcDrivePoints(worldModel, deadline);
      end = System.currentTimeMillis();
      if(end-start>0)
        System.err.println(" ## getDrivePoints.calcDrivePoints.time: "+(end-start));

		  
			if ((drivePoints == null) || (drivePoints.size()==0)) {
			  System.err.println("Could not find driveline for cluster: "+id);
			  if(drivePoints == null) {
			    drivePoints = new ArrayList<Point>();
			  }
			  
			  start = System.currentTimeMillis();
			  Point oppositeDirection = calcOppositeDirection(worldModel, deadline);
	      end = System.currentTimeMillis();
	      if(end-start>0)
	        System.err.println(" ## getDrivePoints.calcOppositeDirection.time: "+(end-start));

			  if (oppositeDirection == null) {
				  for(IAgentDescription iad : shepherds.keySet()) {
				    drivePoints.add(center);
				  }
			    return center;
			  }
			  Point ret = new Point(center.x, center.y);
			  
			  // Point ret = new Point(center.x + oppositeDirection.x, center.y+oppositeDirection.y);
		  
			  start = System.currentTimeMillis();
			  int whileCount = 0; 
			  while ((WorldModel.staticWorldModel.getCell(ret).edgeValue != 0) && (whileCount <3)) {
			    whileCount++;
          Point newRet = new Point(center.x + oppositeDirection.x, center.y + oppositeDirection.y);

          if (worldModel.isInGrid(newRet) && !worldModel.getCell(newRet).isObstacle()
              && !worldModel.getCell(newRet).isSwitch()) {
            ret = newRet;
          } else {
            break;
          }
        }

	       end = System.currentTimeMillis();
	        if(end-start>0)
	          System.err.println(" ## getDrivePoints.while.time: "+(end-start));
			  
        for(IAgentDescription iad : shepherds.keySet()) {
          drivePoints.add(ret);
        }
			  return ret;
//			  return null;
			}
		} 

		start = System.currentTimeMillis();
		if (shepherdPoints == null) {
			shepherdPoints = new HashMap<IAgentDescription, Point>(shepherds
					.size());
			ArrayList<Cell> shepherdCells = new ArrayList<Cell>();

			for (Cell cell : shepherds.values()) {
				shepherdCells.add(cell);
			}

			for (int i = 0; i < drivePoints.size(); i++) {
				Cell[] cells = shepherdCells.toArray(new Cell[shepherdCells
						.size()]);
				final Point foo = drivePoints.get(i);
				Arrays.sort(cells, new Comparator<Cell>() {
					public int compare(Cell a, Cell b) {
						int distA = foo.maxNorm(a);
						int distB = foo.maxNorm(b);
						if (distA == distB)
							return 0;
						else if (distA > distB)
							return 1;
						else
							return -1;
					}
				});
				if (cells.length != 0) {
					shepherdCells.remove(cells[0]);
					// for (int j = 0; j < cells.length; j++)
					// {
					// if (!shepherdPoints.containsValue(cells[j]))
					// {
					//
					// shepherdPoints.put(getIAgentDescription(cells[j]),
					// drivePoints.get(i));
					// break;
					// }
					// }
					shepherdPoints.put(getIAgentDescription(cells[0]),
							drivePoints.get(i));
				} else {
					// FIXME sometimes more drive points than shepherds
					// System.err.println("Cluster: " + id
					// + " cells.length = 0 drivePoints.size() "
					// + drivePoints.size() + " shepherdCells.size(): "
					// + shepherdCells.size() + " shepherds.size():"
					// + shepherds.size());
				}
			}
		}
		 end = System.currentTimeMillis();
     if(end-start>0)
       System.err.println(" ## getDrivePoints.shepherds.time: "+(end-start));
		
		if (shepherdPoints.containsKey(agent)) {
			return shepherdPoints.get(agent);
		} else {
			System.err.println("Cluster: " + id
					+ " shepherdPoints does not contain shepherd "
					+ agent.getName() + " shepherdPoints.size "
					+ shepherdPoints.size() + " drivePoints.size "
					+ drivePoints.size() + " shepherds.size "
					+ shepherds.size());

			return null;
		}
	}

	private IAgentDescription getIAgentDescription(Cell cell) {
		for (IAgentDescription agent : shepherds.keySet()) {
			if (cell.equals(shepherds.get(agent)))
				return agent;
		}
		System.err.println("Cluster.getIAgentDescription() returns null");
		return null;
	}

	public Node assurePath(WorldModel worldModel, long deadline) {
		if (start != null) {
//			System.err.println("\t\t\t### assurePath.start exists");
		  return start;
		}

//		System.err.println("\t\t\t### assurePath.start does NOT exist");
		return calcPath(worldModel, deadline);
	}

	private synchronized Node calcPath(WorldModel worldModel, long deadline) {
		Point center = getCenter();
		Point corralCenter = worldModel.getCorralCenter();
		AStarHeapCow astarCow = new AStarHeapCow(worldModel, center,
				corralCenter);
		astarCow.setAvoidAgents(false);
		astarCow.setAvoidCows(false);
		astarCow.setAvoidClosedFences(false);
		astarCow.setAvoidOpenFences(false);
		start = astarCow.AStar(deadline);
		//
		// Node curr = start;
		// while(curr != null)
		// {
		// worldModel.getCell(curr).setMarked(Cell.MAGENTA_CIRCLE);
		// curr = curr.getPredecessor();
		// }
		return start;
	}

	public String toString() {
		String s = "";
		for (Cell cell : list) {
			s += cell.toStringShort();
		}
		return s;
	}

	public boolean isInCorral() {
		for (Cell c : list) {
			if (!c.isAllyCorral()) {
				return false;
			}
		}
		return true;
	}

	public Cell getNearstCowToDensityCenter() {
		Point center = getDensityCenter();
		int distanz = Integer.MAX_VALUE;
		Cell nearst = null;
		for (Cell cow : list) {
			int tmp = cow.maxNorm(center);
			if (tmp < distanz) {
				nearst = cow;
				distanz = tmp;
			}
		}

		return nearst;
	}

	public Point getOppositeDirection(WorldModel worldModel, long deadline) {
		if (oppositeDirection == null) {
			oppositeDirection = calcOppositeDirection(worldModel, deadline);
		}
		return oppositeDirection;
	}

	private Point calcOppositeDirection(WorldModel worldModel, long deadline) {
		assurePath(worldModel, deadline);
		if (start == null) {
			System.out.println("calcDirection.start = null");
			return null;
		}

		Node firstStep = start.getPredecessor();
		if (firstStep == null) {
			System.out.println("calcDirection.firstStep = null");
			return null;
		}
		int xdirection, ydirection;

		xdirection = (start.getX() - firstStep.getX());
		ydirection = (start.getY() - firstStep.getY());

		return new Point(xdirection, ydirection);
	}

	public void calculateSmoothValue() {
		smoothValue = 0;
		for (Cell cell:list) {
			smoothValue += cell.edgeValue;
		}
	}

	public boolean isEnemyCorral() {
		for (Cell c:list) {
			if (c.isEnemyCorral()) {
				return true;
			}
		}
		return false;
	}
	// private Point simpleDirection (Node start)
	// {
	// Node firstStep = start.getPredecessor();
	// Node secondStep = null;
	// if (firstStep != null)
	// {
	// secondStep = firstStep.getPredecessor();
	// }
	// else
	// {
	// return null;
	// }
	//
	// int xdirection, ydirection;
	// if (secondStep != null)
	// {
	// xdirection = start.getX() - secondStep.getX();
	// ydirection = start.getY() - secondStep.getY();
	// }
	// else
	// {
	// // *2 so that a direction always has the maxNorm 2
	// xdirection = (start.getX() - firstStep.getX()) * 2;
	// ydirection = (start.getY() - firstStep.getY()) * 2;
	// }
	//
	// return new Point(xdirection, ydirection);
	// }
}
